﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
using System.Collections;

namespace Catalyst.Cloning
{
	/// <summary>
	/// Enumeration that defines the type of cloning of a field.
	/// Used in combination with the CloneAttribute
	/// </summary>
	public enum CloneType
	{
		None,
		ShallowCloning,
		DeepCloning
	}

	/// <summary>
	/// CloningAttribute for specifying the cloneproperties of a field.
	/// </summary>
	[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
	public class CloneAttribute : Attribute
	{
		private CloneType _clonetype;
		public CloneAttribute()
		{
			
		}

		public CloneType CloneType
		{
			get { return _clonetype; }
			set { _clonetype = value; }
		}
	}

	/// <summary>
	/// Class that clones objects
	/// </summary>
	/// <remarks>
	/// Currently can deepclone to 1 level deep.
	/// Ex. Person.Addresses (Person.List<Address>) 
	/// -> Clones 'Person' deep
	/// -> Clones the objects of the 'Address' list deep
	/// -> Clones the sub-objects of the Address object shallow. (at the moment)
	/// </remarks>
	public static class CloneHelper<T>
	where T : class
	{
		#region Declarations
		// Dictionaries for caching the (pre)compiled generated IL code.
		private static Dictionary<Type, Delegate> _cachedILShallow = new Dictionary<Type, Delegate>();
		private static Dictionary<Type, Delegate> _cachedILDeep = new Dictionary<Type, Delegate>();
		// This is used for setting the fixed cloning, of this is null, then
		// the custom cloning should be invoked. (use Clone(T obj) for custom cloning)
		private static CloneType? _globalCloneType = CloneType.ShallowCloning;

		#endregion

		#region Public Methods

		/// <summary>
		/// Clone an object with Deep Cloning or with a custom strategy 
		/// such as ShallowCloning and/or DeepCloning combined (use the CloneAttribute)
		/// </summary>
		/// <param name="obj">Object to perform cloning on.</param>
		/// <returns>Cloned object.</returns>
		public static T Clone(T obj)
		{
			_globalCloneType = null;
			return CloneObjectWithILDeep(obj);
		}

		/// <summary>
		/// Clone an object with one strategy (DeepClone or ShallowClone)
		/// </summary>
		/// <param name="obj">Object to perform cloning on.</param>
		/// <param name="cloneType">Type of cloning</param>
		/// <returns>Cloned object.</returns>
		/// <exception cref="InvalidOperationException">When a wrong enum for cloningtype is passed.</exception>
		public static T Clone(T obj, CloneType cloneType)
		{
			if (_globalCloneType != null)
				_globalCloneType = cloneType;
			switch (cloneType)
			{
				case CloneType.None:
					throw new InvalidOperationException("No need to call this method?");
				case CloneType.ShallowCloning:
					return CloneObjectWithILShallow(obj);
				case CloneType.DeepCloning:
					return CloneObjectWithILDeep(obj);
				default:
					break;
			}
			return default(T);
		}

		#endregion

		#region Private Methods

		/// <summary>    
		/// Generic cloning method that clones an object using IL.    
		/// Only the first call of a certain type will hold back performance.    
		/// After the first call, the compiled IL is executed.    
		/// </summary>    
		/// <typeparam name="T">Type of object to clone</typeparam>    
		/// <param name="myObject">Object to clone</param>    
		/// <returns>Cloned object (shallow)</returns>    
		private static T CloneObjectWithILShallow(T myObject)
		{
			Delegate myExec = null;
			if (!_cachedILShallow.TryGetValue(typeof(T), out myExec))
			{
				DynamicMethod dymMethod = new DynamicMethod("DoShallowClone", typeof(T), new Type[] { typeof(T) }, Assembly.GetExecutingAssembly().ManifestModule, true);
				ConstructorInfo cInfo = myObject.GetType().GetConstructor(new Type[] { });
				ILGenerator generator = dymMethod.GetILGenerator();
				LocalBuilder lbf = generator.DeclareLocal(typeof(T));
				generator.Emit(OpCodes.Newobj, cInfo);
				generator.Emit(OpCodes.Stloc_0);
				foreach (FieldInfo field in myObject.GetType().GetFields(System.Reflection.BindingFlags.Instance
				| System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public))
				{
					generator.Emit(OpCodes.Ldloc_0);
					generator.Emit(OpCodes.Ldarg_0);
					generator.Emit(OpCodes.Ldfld, field);
					generator.Emit(OpCodes.Stfld, field);
				}
				generator.Emit(OpCodes.Ldloc_0);
				generator.Emit(OpCodes.Ret);
				myExec = dymMethod.CreateDelegate(typeof(Func<T, T>));
				_cachedILShallow.Add(typeof(T), myExec);
			}
			return ((Func<T, T>)myExec)(myObject);
		}

		/// <summary>
		/// Generic cloning method that clones an object using IL.
		/// Only the first call of a certain type will hold back performance.
		/// After the first call, the compiled IL is executed. 
		/// </summary>
		/// <param name="myObject">Type of object to clone</param>
		/// <returns>Cloned object (deeply cloned)</returns>
		private static T CloneObjectWithILDeep(T myObject)
		{
			Delegate myExec = null;
			if (!_cachedILDeep.TryGetValue(typeof(T), out myExec))
			{
				// Create ILGenerator            
				DynamicMethod dymMethod = new DynamicMethod("DoDeepClone", typeof(T), new Type[] { typeof(T) }, Assembly.GetExecutingAssembly().ManifestModule, true);
				ILGenerator generator = dymMethod.GetILGenerator();
				LocalBuilder cloneVariable = generator.DeclareLocal(myObject.GetType());

				ConstructorInfo cInfo = myObject.GetType().GetConstructor(Type.EmptyTypes);
				generator.Emit(OpCodes.Newobj, cInfo);
				generator.Emit(OpCodes.Stloc, cloneVariable);

				// REM by Ashley - if only checking fields in typeof(T), then the object being cloned must be explicitly specified
				//foreach (FieldInfo field in typeof(T).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public))
				foreach (FieldInfo field in myObject.GetType().GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public))
				{
					if (_globalCloneType == CloneType.DeepCloning)
					{
						if (field.FieldType.IsValueType || field.FieldType == typeof(string))
						{
							generator.Emit(OpCodes.Ldloc, cloneVariable);
							generator.Emit(OpCodes.Ldarg_0);
							generator.Emit(OpCodes.Ldfld, field);
							generator.Emit(OpCodes.Stfld, field);
						}
						else if (field.FieldType.IsClass)
						{
							CopyReferenceType(generator, cloneVariable, field);
						}
					}
					else
					{
						switch (GetCloneTypeForField(field))
						{
							case CloneType.ShallowCloning:
								{
									generator.Emit(OpCodes.Ldloc, cloneVariable);
									generator.Emit(OpCodes.Ldarg_0);
									generator.Emit(OpCodes.Ldfld, field);
									generator.Emit(OpCodes.Stfld, field);
									break;
								}
							case CloneType.DeepCloning:
								{
									if (field.FieldType.IsValueType || field.FieldType == typeof(string))
									{
										generator.Emit(OpCodes.Ldloc, cloneVariable);
										generator.Emit(OpCodes.Ldarg_0);
										generator.Emit(OpCodes.Ldfld, field);
										generator.Emit(OpCodes.Stfld, field);
									}
									else if (field.FieldType.IsClass)
										CopyReferenceType(generator, cloneVariable, field);
									break;
								}
							case CloneType.None:
								{
									// Do nothing here, field is not cloned.
								}
								break;
						}
					}
				}
				generator.Emit(OpCodes.Ldloc_0);
				generator.Emit(OpCodes.Ret);
				myExec = dymMethod.CreateDelegate(typeof(Func<T, T>));
				_cachedILDeep.Add(typeof(T), myExec);
			}
			return ((Func<T, T>)myExec)(myObject);
		}

		/// <summary>
		/// Helper method to clone a reference type.
		/// This method clones IList and IEnumerables and other reference types (classes)
		/// Arrays are not yet supported (ex. string[])
		/// </summary>
		/// <param name="generator">IL generator to emit code to.</param>
		/// <param name="cloneVar">Local store wheren the clone object is located. (or child of)</param>
		/// <param name="field">Field definition of the reference type to clone.</param>
		private static void CopyReferenceType(ILGenerator generator, LocalBuilder cloneVar, FieldInfo field)
		{
			if (field.FieldType.IsSubclassOf(typeof(Delegate)))
			{
				return;
			}
			LocalBuilder lbTempVar = generator.DeclareLocal(field.FieldType);

			if (field.FieldType.GetInterface("IEnumerable") != null && field.FieldType.GetInterface("IList") != null)
			{
				if (field.FieldType.IsGenericType)
				{
					Type argumentType = field.FieldType.GetGenericArguments()[0];
					Type genericTypeEnum = Type.GetType("System.Collections.Generic.IEnumerable`1[" + argumentType.FullName + "]");

					ConstructorInfo ci = field.FieldType.GetConstructor(new Type[] { genericTypeEnum });
					if (ci != null && GetCloneTypeForField(field) == CloneType.ShallowCloning)
					{
						generator.Emit(OpCodes.Ldarg_0);
						generator.Emit(OpCodes.Ldfld, field);
						generator.Emit(OpCodes.Newobj, ci);
						generator.Emit(OpCodes.Stloc, lbTempVar);
						generator.Emit(OpCodes.Ldloc, cloneVar);
						generator.Emit(OpCodes.Ldloc, lbTempVar);
						generator.Emit(OpCodes.Stfld, field);
					}
					else
					{
						ci = field.FieldType.GetConstructor(Type.EmptyTypes);
						if (ci != null)
						{
							generator.Emit(OpCodes.Newobj, ci);
							generator.Emit(OpCodes.Stloc, lbTempVar);
							generator.Emit(OpCodes.Ldloc, cloneVar);
							generator.Emit(OpCodes.Ldloc, lbTempVar);
							generator.Emit(OpCodes.Stfld, field);
							CloneList(generator, field, argumentType, lbTempVar);
						}
					}
				}
			}
			else
			{
				ConstructorInfo cInfo = field.FieldType.GetConstructor(new Type[] { });
				generator.Emit(OpCodes.Newobj, cInfo);
				generator.Emit(OpCodes.Stloc, lbTempVar);
				generator.Emit(OpCodes.Ldloc, cloneVar);
				generator.Emit(OpCodes.Ldloc, lbTempVar);
				generator.Emit(OpCodes.Stfld, field);
				foreach (FieldInfo fi in field.FieldType.GetFields(System.Reflection.BindingFlags.Instance
					| System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public))
				{
					if (fi.FieldType.IsValueType || fi.FieldType == typeof(string))
					{
						generator.Emit(OpCodes.Ldloc_1);
						generator.Emit(OpCodes.Ldarg_0);
						generator.Emit(OpCodes.Ldfld, field);
						generator.Emit(OpCodes.Ldfld, fi);
						generator.Emit(OpCodes.Stfld, fi);
					}
				}
			}
		}

		/// <summary>
		/// Makes a deep copy of an IList of IEnumerable
		/// Creating new objects of the list and containing objects. (using default constructor)
		/// And by invoking the deepclone method defined above. (recursive)
		/// </summary>
		/// <param name="generator">IL generator to emit code to.</param>
		/// <param name="listField">Field definition of the reference type of the list to clone.</param>
		/// <param name="typeToClone">Base-type to clone (argument of List<T></param>
		/// <param name="cloneVar">Local store wheren the clone object is located. (or child of)</param>
		private static void CloneList(ILGenerator generator, FieldInfo listField, Type typeToClone, LocalBuilder cloneVar)
		{
			Type genIEnumeratorTyp = Type.GetType("System.Collections.Generic.IEnumerator`1[" + typeToClone.FullName + "]");
			Type genIEnumeratorTypLocal = Type.GetType(listField.FieldType.Namespace + "." + listField.FieldType.Name + "+Enumerator[[" + typeToClone.FullName + "]]");
			LocalBuilder lbEnumObject = generator.DeclareLocal(genIEnumeratorTyp);
			LocalBuilder lbCheckStatement = generator.DeclareLocal(typeof(bool));
			Label checkOfWhile = generator.DefineLabel();
			Label startOfWhile = generator.DefineLabel();
			MethodInfo miEnumerator = listField.FieldType.GetMethod("GetEnumerator");
			generator.Emit(OpCodes.Ldarg_0);
			generator.Emit(OpCodes.Ldfld, listField);
			generator.Emit(OpCodes.Callvirt, miEnumerator);
			if (genIEnumeratorTypLocal != null)
			{
				generator.Emit(OpCodes.Box, genIEnumeratorTypLocal);
			}
			generator.Emit(OpCodes.Stloc, lbEnumObject);
			generator.Emit(OpCodes.Br_S, checkOfWhile);
			generator.MarkLabel(startOfWhile);
			generator.Emit(OpCodes.Nop);
			generator.Emit(OpCodes.Ldloc, cloneVar);
			generator.Emit(OpCodes.Ldloc, lbEnumObject);
			MethodInfo miCurrent = genIEnumeratorTyp.GetProperty("Current").GetGetMethod();
			generator.Emit(OpCodes.Callvirt, miCurrent);
			Type cloneHelper = Type.GetType(typeof(CloneHelper<T>).Namespace + "." + typeof(CloneHelper<T>).Name + "[" + miCurrent.ReturnType.FullName + "]");
			MethodInfo miDeepClone = cloneHelper.GetMethod("CloneObjectWithILDeep", BindingFlags.Static | BindingFlags.NonPublic);
			generator.Emit(OpCodes.Call, miDeepClone);
			MethodInfo miAdd = listField.FieldType.GetMethod("Add");
			generator.Emit(OpCodes.Callvirt, miAdd);
			generator.Emit(OpCodes.Nop);
			generator.MarkLabel(checkOfWhile);
			generator.Emit(OpCodes.Nop);
			generator.Emit(OpCodes.Ldloc, lbEnumObject);
			MethodInfo miMoveNext = typeof(IEnumerator).GetMethod("MoveNext");
			generator.Emit(OpCodes.Callvirt, miMoveNext);
			generator.Emit(OpCodes.Stloc, lbCheckStatement);
			generator.Emit(OpCodes.Ldloc, lbCheckStatement);
			generator.Emit(OpCodes.Brtrue_S, startOfWhile);
		}

		/// <summary>
		/// Returns the type of cloning to apply on a certain field when in custom mode.
		/// Otherwise the main cloning method is returned.
		/// You can invoke custom mode by invoking the method Clone(T obj)
		/// </summary>
		/// <param name="field">Field to examine</param>
		/// <returns>Type of cloning to use for this field.</returns>
		private static CloneType GetCloneTypeForField(FieldInfo field)
		{
			object[] attributes = field.GetCustomAttributes(typeof(CloneAttribute), true);
			if (attributes == null || attributes.Length == 0)
			{
				if (!_globalCloneType.HasValue)
					return CloneType.ShallowCloning;
				else
					return _globalCloneType.Value;
			}
			return (attributes[0] as CloneAttribute).CloneType;
		}

		#endregion
	}
} 
